package com.ydd.common.aop;


import com.alibaba.nacos.client.naming.utils.SignUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ydd.common.annotation.RepeatSubmit;
import com.ydd.common.constent.AuthConstant;
import com.ydd.common.exception.BusinessException;
import com.ydd.common.result.ResponseResult;
import com.ydd.common.utils.JsonUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;

@Aspect
@RequiredArgsConstructor
@Component
@Slf4j
public class RepeatSubmitAspect {
    private final RedisTemplate<String, Object> redisTemplate;

    @Pointcut("@annotation(repeatSubmit)")
    public void pointNoRepeatSubmit(RepeatSubmit repeatSubmit) {
    }

    /**
     * 利用Redis实现的防重复提交拦截器。
     *
     * @param joinPoint    切面连接点，表示被拦截的方法。
     * @param repeatSubmit 重复提交注解对象，包含锁的时间等配置。
     * @return 返回方法执行结果，若重复提交则返回失败信息。
     * @throws Throwable 如果方法执行过程中出现异常，则抛出。
     */
    @Around(value = "pointNoRepeatSubmit(repeatSubmit)")
    public Object repeatSubmit(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 获取当前请求的属性
        if (attributes == null) {
            log.error("ServletRequestAttributes is null.");
            return ResponseResult.error("系统异常");
        }
        String key = buildKey(request, repeatSubmit);

        // 尝试加锁，防止重复提交
        if (!tryLock(key, repeatSubmit.lockTime())) {
            String repeatMsg = "请勿重复提交或者操作过于频繁！ 请在" + repeatSubmit.lockTime() + "秒后重试";
            log.info(repeatMsg);
            return ResponseResult.error(repeatMsg);
        }


        return joinPoint.proceed();

    }

    /**
     * 尝试对给定的键加锁。
     *
     * @param key      键名，用于Redis中标识一个锁。
     * @param lockTime 锁定的时间，单位秒。
     * @return 如果加锁成功返回true，否则返回false。
     */
    private boolean tryLock(String key, Long lockTime) {
        Boolean locked = redisTemplate.opsForValue().setIfAbsent(key, key);
        if (Boolean.TRUE.equals(locked)) {
            redisTemplate.expire(key, lockTime, TimeUnit.SECONDS);
            return true;
        }
        return false;
    }

    /**
     * 根据请求信息和注解配置构建唯一键。
     *
     * @param request      HttpServletRequest对象，用于获取请求信息。
     * @param repeatSubmit 重复提交注解对象，配置限制类型等。
     * @return 返回构建好的唯一键字符串。
     */
    private String buildKey(HttpServletRequest request, RepeatSubmit repeatSubmit) {
        StringBuilder key = new StringBuilder();
        String limitType = repeatSubmit.limitType().name();
        String group = repeatSubmit.key();
        // 根据限制类型构建键名
        if (limitType.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) {
            key.append("lock:").append(group).append("--").append(getToken(request)).append(request.getRequestURI());
        }
        log.info("防止重复提交Key:{}", key);
        return key.toString();
    }

    /**
     * 从HTTP请求中获取用户认证令牌
     * <p>
     * 此方法通过请求头中的特定键来提取用户认证令牌，该键由AuthConstant.TOKEN_HEADER定义
     * 它主要用于身份验证过程，以获取客户端发送的令牌信息
     *
     * @param request HTTP请求对象，用于从请求头中获取令牌信息
     * @return 返回提取出的令牌字符串如果请求头中没有令牌，则返回空字符串
     */
    private String getToken(HttpServletRequest request) {
        // 从请求头中获取令牌信息，AuthConstant.TOKEN_HEADER定义了令牌对应的请求头键
        String token = request.getHeader(AuthConstant.TOKEN);
        // 返回提取出的令牌字符串
        if (StringUtils.isEmpty(token)) {
            throw new BusinessException("token不能为空");
        }
        return token;
    }

}
